#!/usr/bin/ruby
#
# Copyright:: Copyright 2017, Trimble Inc.
# License:: All Rights Reserved.
# Original Author:: Ross Williamson (mailto:ross@sketchup.com)
#
# The purpose of this file is to convert the older generate report
# code to use the newer HTML dialog with Chromium. A lot of it is copy pasted
# from the old functions so that we could avoid changing anything
# in the old dynamic components code. We were too afraid of breaking anything.
#
# Summary of Changes from the old code:
#
# 1) Raw JSON values are returned to JavaScript instead of the js_callback()
#    functionality used before.
# 2) WebDialog converted to HTMLDialog
# 3) All manual JSON creation was removed. The old code wasn't actually
#    producing valid json - because it would pass data through JavaScript
#    objects, so I converted all of that manual construction to use
#    the to_json built in function.

require "sketchup.rb"
require "langhandler.rb"

Sketchup::require "su_dynamiccomponents/ruby/dcclass_v1"

class GenerateReport
  DEFAULT_REPORT_WIDTH = 1024
  DEFAULT_REPORT_HEIGHT = 768
  DEFAULT_REPORT_TOP = 50
  DEFAULT_REPORT_LEFT = 50

  DIALOG_NAME = "Generate Report"

  @@is_debug = false

  def initialize(dc_instance)
    @dc = dc_instance
    name = @dc.translate(DIALOG_NAME)
    @dialog = UI::HtmlDialog.new({
      :dialog_title => @dc.translate(DIALOG_NAME),
      :preferences_key => DIALOG_NAME,
      :scrollable => true,
      :resizable => true,
      :width => DEFAULT_REPORT_WIDTH,
      :height => DEFAULT_REPORT_HEIGHT,
      :left => DEFAULT_REPORT_LEFT,
      :top => DEFAULT_REPORT_TOP,
      :style => UI::HtmlDialog::STYLE_DIALOG,
    })

    # store a hash of entity pointers so we can get to them by ID later
    @entity_pointers = {}

    # Matches @dictionary_name in @dc
    @dictionary_name = "dynamic_attributes"

    @dialog.add_action_callback("pull_information") { |action_context|
      pull_information()
    }

    @dialog.add_action_callback("read_templates") { |action_context|
      read_templates()
    }

    @dialog.add_action_callback("delete_template") { |action_context, id|
      delete_template(id)
    }

    @dialog.add_action_callback("do_close") { |action_context|
      close_dialog()
    }

    @dialog.add_action_callback("save_report") {
      |action_context, filetype, report_string|
      save_report(filetype, report_string)
    }

    @dialog.add_action_callback("update_template") {
      |action_context, template_id, template_body|
      update_template(template_id, template_body)
    }

    @dialog.add_action_callback("pull_attribute_tree") {
      |action_context, selection_ids, dictionary, deep, max_recursion_depth|
      params = {
        :selection_ids => selection_ids,
        :dictionary => dictionary,
        :deep => deep,
        :max_recursion_depth => max_recursion_depth,
      }
      pull_attribute_tree(params)
    }

    @dialog.add_action_callback("pull_tag_folder_hierarchy") { |action_context|
      pull_tag_folder_hierarchy
    }

    if @@is_debug
      debug_command = UI::Command.new("Dump Generate Report JSON...") {
        params = {
          :selection_ids => "active_model",
          :dictionary => "dynamic_attributes",
          :deep => true,
          :max_recursion_depth => "-1",
        }
        skp_filename = File.basename(Sketchup.active_model.path)
        default_save_name = skp_filename + ".json"
        path = UI.savepanel "", nil, default_save_name
        if path
          json = pull_attribute_tree(params)
          file = File.new(path, "w")
          file.print json
          file.close
          puts "Finished writing file #{path}"
        end
      }
      file_menu = UI.menu("File")
      file_menu.add_item debug_command
    end
  end

  def self.set_debug(arg)
    @@is_debug = arg
    return "Open or Reopen the Generate Report " +
             "Dialog to see the debug file menu option"
  end

  def is_visible()
    return @dialog.visible?
  end

  def show_dialog()
    @dialog.show()
    if Sketchup.is_online == true
      url = Sketchup.get_datfile_info "GENERATE_REPORT",
                                      "https://3dwarehouse.sketchup.com/2018_generatereport.html"
    else
      url = File.join($Ahtml目录, "reporter.html")
    end
    # If there is no 'http:' or 'https:' in the url, calculate a relative path
    if url.index(/https*:/) != 0
      # Do a search for the file we want to display
      path_array = url.split(/\//)
      file_to_find = path_array.pop
      path_to_search = path_array.join("/")
      # found_file = Sketchup.find_support_file file_to_find, path_to_search
      found_file = File.join(path_to_search, file_to_find)
      @dialog.set_file(found_file)
    else
      @dialog.set_url url
    end
  end

  def close_dialog()
    @dialog.close()
  end

  def save_report(filetype, report_string)
    # Convert newlines if they were encoded.
    report_string = report_string.gsub("&#13;", '\n')
    @dc.save_report_to_file(report_string, filetype)

    return {
             :success => true,
           }.to_json
  end

  def delete_template(template_id)
    template_dict = @dc.get_template_dictionary()
    if template_dict[template_id].nil?
      success = false
      message = "TEMPLATE_NOT_FOUND"
    else
      template_dict.delete_key(template_id)
      success = true
    end
    return {
             :success => success,
             :message => message,
           }.to_json
  end

  def update_template(template_id, template_body)
    template_dict = @dc.get_template_dictionary()
    template_dict[template_id] = template_body
    return {
             :success => true,
             :id => template_id,
           }.to_json
  end

  # List parent tag folders for each tag.
  #
  # @return [String] JSON formatted array.
  #   ```
  #   [
  #     ["Tag1"],
  #     ["Tag2"],
  #     ["Tag Folder", "Tag Subfolder", "Tag3"],
  #     ["Tag Folder", "Tag4"]
  #   ]
  #   ```
  def pull_tag_folder_hierarchy
    layers = Sketchup.active_model.layers
    return [] unless layers.respond_to?(:folders)

    tag_data = layers.map do |tag|
      tag_parents(tag).reverse.map(&:name)
    end

    tag_data.to_json
  end

  # List tag along with its parents.
  #
  # @param [Sketchup::Layer]
  #
  # @return [Array<Sketchup::Layer, Sketchup::LayerFolder>]
  #   Tag itself followed by its parent, ordered deepest to highest.
  def tag_parents(tag)
    trace = [tag]
    while trace.last.folder
      trace << trace.last.folder
    end

    trace
  end

  def pull_information()
    json_hash = {
      :version => Sketchup.version.to_s,
      :language => Sketchup.get_locale.to_s,
      :is_pro => Sketchup.is_pro?.to_s,
      :dc_version => $dc_extension.version.to_s,
      :units => @dc.get_default_units,
      :decimal_delimiter => @dc.get_decimal_delimiter,
      :temp_path => File.expand_path(@dc.get_temp_path),
      :simplify => @simplify.to_s,
    }

    dc_help_url = Sketchup.get_datfile_info "DC_HELP",
                                            "http://help.sketchup.com/article/115543"

    dc_functions_url = Sketchup.get_datfile_info "DC_FUNCTIONS",
                                                 "http://help.sketchup.com/article/108144"

    dc_online_url = Sketchup.get_datfile_info "GENERATE_REPORT",
                                              "https://3dwarehouse.sketchup.com/generatereport.html"

    json_hash[:dc_help_url] = dc_help_url
    json_hash[:dc_functions_url] = dc_functions_url
    json_hash[:dc_online_url] = dc_online_url

    # Add information regarding default formatting.
    options = Sketchup.active_model.options["UnitsOptions"]
    json_hash[:length_format] = @dc.get_length_format
    json_hash[:length_unit] = @dc.get_length_unit
    json_hash[:length_precision] = options["LengthPrecision"].to_s
    json_hash[:angle_precision] = options["AnglePrecision"].to_s
    strings_hash = {}

    $dc_strings.GetStrings().keys.each do |string|
      strings_hash[string] = $dc_strings.GetString(string)
    end

    json_hash[:strings] = strings_hash
    return json_hash.to_json
  end

  def read_templates()
    template_dict = @dc.get_template_dictionary()

    templates = []
    template_dict.each { |key, value|
      templates.push(value)
    }
    json_hash = {
      :entries => templates,
    }
    return json_hash.to_json
  end

  def pull_attribute_tree(params)
    ids = params[:selection_ids]
    # Check to see if we were passed a dictionary to pull, or determine
    # default behavior if nothing was passed.
    if params[:dictionary].to_s == "all_dictionaries"
      specific_dictionary = nil
    else
      specific_dictionary = @dc.second_if_empty(params[:dictionary],
                                                @dictionary_name)
    end

    # Construct our list of entities. If there is no active model, then there
    # can't be any entities.
    if Sketchup.active_model == nil
      entities = []
    elsif (ids.nil?)

      # If components are selected, then take those as our entities. If not,
      # take the current edit context.
      if Sketchup.active_model.selection.length > 0
        entities = []
        for entity in Sketchup.active_model.selection
          if entity.is_a?(Sketchup::ComponentInstance) ||
             entity.is_a?(Sketchup::Group)
            # Do not allow work against any components that
            # have a versioning error. (Meaning they are a version of
            # DCs that this plugin does not know how to handle.)
            if @dcobservers.has_version_error(entity) == false
              entities.push entity
            end
          end
        end
        if entities.length == 0 && Sketchup.active_model.active_path != nil
          entities = [Sketchup.active_model.active_path.pop]
        end
      elsif Sketchup.active_model.active_path != nil
        entities = [Sketchup.active_model.active_path.pop]
      end
    elsif ids.downcase == "active_model"
      entities = []
      entities.push Sketchup.active_model
    elsif ids.downcase == "selection"
      entities = Sketchup.active_model.selection
    else
      ids = ids.split(",")
      len = ids.length

      # TODO(thomthom) This is so badin so many ways. @entity_pointers is
      # tracking entities globally and never clears them. So it holds on to
      # stale data. I don't dare to clear then in the ModelObserver when a new
      # model is opened/created because that won't play nice with OSX's MDI.
      #
      # Additionally, it searches the selection and comparing strings - which is
      # dead slow - as with many other operations in DC.
      #
      # The real fix is a refactor, but we're days away from SU2014 M0 release
      # so as a quick and very dirty "fix" we prune the @entity_pointers cache
      # here so it won't raise errors with out template components. Their entity
      # ID is reused, so after the first model under Windows the cache will
      # point to deleted entities.
      @entity_pointers.delete_if { |key, value| value.deleted? }

      entities = ids.collect do |id|
        @entity_pointers[id] || Sketchup.active_model.selection.detect do |el|
          el.entityID.to_s == id
        end
      end
    end
    json = nil
    formulas = params[:run_formulas]

    deep = params[:deep]
    deep = false if deep.nil?

    # By default the current_depth is 0 and the max_recursion_depth is 1.
    current_depth = 0
    max_recursion_depth = 1
    if !params[:max_recursion_depth].nil?
      max_recursion_depth = params[:max_recursion_depth].to_i
    end

    attrs = []
    if entities != nil
      entities.each do |entity|
        next if entity.nil?
        next if entity.valid? == false
        next if !entity.is_a?(Sketchup::ComponentInstance) &&
                !entity.is_a?(Sketchup::Model) &&
                !entity.is_a?(Sketchup::Group) &&
                !entity.is_a?(Sketchup::ComponentDefinition)

        if (formulas == true)
          # Since the JS is no longer requesting individual formula results for
          # each refresh, run all of our formulas here, which will update
          # values and store any parser errors into the JSON data so errors
          # display
          @dc.run_all_formulas(entity)
        end
        attrs.push(entity_to_json_hash(entity, deep, specific_dictionary, true,
                                       current_depth, max_recursion_depth))
      end
    end

    json_hash = {
      :entities => attrs,
    }
    return json_hash.to_json
  end

  def entity_to_json(entity, do_recursion = true,
                             specific_dictionary = nil, nest_meta_attributes = true,
                             current_depth = 0, max_recursion_depth = 1)
    return entity_to_json_hash(entity, do_recursion, specific_dictionary,
                               nest_meta_attributes, current_depth, max_recursion_depth).to_json
  end

  # Creates a JSON string respresentation of an entity and its children
  #
  #   Args:
  #      entity: the entity to convert
  #      do_recursion: boolean stating whether to pull in children as well
  #      specific_dictionary: optional string name of the attribute dictionary
  #        to output. If this is set, then the JSON is guaranteed to have an
  #        entry for this dictionary, even if the dictionary doesn't exist on
  #        the entity. If it is nil, then we will output all dictionaries found.
  #      nest_meta_attributes: optional boolean stating whether to return meta
  #        attributes (such as "_x_formula") as friendly parsed "nested"
  #        subvariables rather than raw name, value pairs. Defaults to true.
  #      current_depth: optional integer that tracks the current depth of the
  #        entity from the root. The default is zero.
  #      max_recusion_depth: optional integer that determines when recursion
  #        should stop. The default is one, which will deliver the current
  #        entity and its children. Use -1 to build the entire tree.
  #
  #   Returns:
  #      string: a JSON string ready for parsing with javascript
  def entity_to_json_hash(entity, do_recursion = true,
                                  specific_dictionary = nil, nest_meta_attributes = true,
                                  current_depth = 0, max_recursion_depth = 1)

    # If we're at the top of a tree, create a hash to keep track of names
    # inside the tree. We'll use this to generate unique names for those
    # elements with overlapping names.
    puts "entity_to_json_hash ing"
    if @has_used_name == nil
      @has_used_name = {}
      clear_names_before_return = true
    else
      clear_names_before_return = false
    end

    # generate basic entity json
    json_hash = {}

    # Cache the entity in `entity_pointers` if it is a Sketchup::Entity.
    # NOTE(Isaac): Sometimes this method is called and entity is actually a
    # Sketchup::Model.
    if entity.is_a?(Sketchup::Entity)
      @entity_pointers[entity.entityID.to_s] = entity
    end

    # If this JSON is being constructed in a DC context (meaning we're
    # requesting an entity's JSON for purposes of applying formulas, etc.) then
    # get the entity's name that is "safe" for the parser.
    if specific_dictionary == @dictionary_name
      this_name = get_entity_formula_name(entity)
    else
      this_name = entity.name
    end

    # Record the fact that we've used this name, so that other members in the
    # tree can avoid colliding with it.
    @has_used_name[this_name] = true
    json_hash[:name] = this_name
    json_hash[:typename] = entity.typename
    json_hash[:id] = entity.entityID.to_s

    if entity.is_a?(Sketchup::ComponentInstance)
      json_hash[:guid] = entity.definition.guid
      json_hash[:definitionID] = entity.definition.entityID
    elsif entity.is_a?(Sketchup::ComponentDefinition) || entity.is_a?(Sketchup::Model)
      json_hash[:guid] = entity.guid
    end

    if entity.is_a?(Sketchup::ComponentDefinition) ||
       entity.is_a?(Sketchup::Model) || entity.is_a?(Sketchup::Group)
      json_hash[:description] = entity.description
    end

    # Add the 3D Warehouse id when available.
    if entity.is_a?(Sketchup::ComponentDefinition) ||
       entity.is_a?(Sketchup::Model)
      model_id = entity.get_attribute("3DWarehouse", "ModelID")
      if !model_id.nil?
        json_hash[:warehouseId] = model_id
      end
    end

    # Add `layer`, `volume` and other default attributes.
    json_hash[:layer] = entity.layer.name

    if entity.is_a?(Sketchup::ComponentInstance) ||
       entity.is_a?(Sketchup::Group)
      volume = @dc.fix_float(entity.volume)
      if volume >= 0
        json_hash[:volume] = volume.to_s
      end
    end

    for attribute_name in ["X", "Y", "Z", "LenX", "LenY", "LenZ", "Material",
                           "Price", "Size", "Url", "Owner", "Status"]
      value = @dc.get_live_value(entity, attribute_name)
      puts "attribute_name 👉 #{attribute_name}"
      puts "value 👉 #{value}"
      if !value.nil?
        json_hash[attribute_name] = value.to_s
      end
    end

    # Add classifier data.
    classifier_data = @dc.get_classifier_data(entity)
    json_hash[:classifier] = classifier_data

    # This is needed for the display of CSS files that have a relative path.
    # For example, if I enter a CSSURL of "mycss.css", then the configure
    # dialog will use this file variable to calculate a local path to the file.
    json_hash[:file] = Sketchup.active_model.path

    if entity.is_a?(Sketchup::Model)
      shadow_info = Sketchup.active_model.shadow_info
      lat = @dc.second_if_empty(shadow_info["Latitude"].to_f, 0.0)
      lng = @dc.second_if_empty(shadow_info["Longitude"].to_f, 0.0)
      json_hash[:latitude] = lat
      json_hash[:longitude] = lng
      bounds = {
        :width => entity.bounds.width.to_f,
        :depth => entity.bounds.depth.to_f,
        :height => entity.bounds.height.to_f,
      }
      json_hash[:bounds] = bounds
    end

    if entity.is_a?(Sketchup::ComponentDefinition)
      json_hash[:countInstances] = entity.count_instances
    end

    if entity.is_a?(Sketchup::ComponentDefinition)
      # Generate json for new attributes
      attrdict = entity.attribute_dictionary("SU_DefinitionSet")
      if !attrdict.nil?
        add_attribute_dictionary_to_json(json_hash, attrdict)
      end
    end

    if entity.is_a?(Sketchup::ComponentInstance)
      # Generate json for new attributes
      attrdict = entity.definition.attribute_dictionary("SU_DefinitionSet")
      if !attrdict.nil?
        add_attribute_dictionary_to_json(json_hash, attrdict)
      end

      attrdict = entity.attribute_dictionary("SU_InstanceSet")
      if !attrdict.nil?
        add_attribute_dictionary_to_json(json_hash, attrdict)
      end
    end

    # Determine which attribute dictionaries to include in the JSON.
    dictionaries_to_output = {}

    # Construct a hash whose keys contain the names of the attribute
    # dictionaries we need to output.
    if specific_dictionary != nil
      dictionaries_to_output[specific_dictionary] = true
    else
      if entity.attribute_dictionaries != nil
        for dict in entity.attribute_dictionaries
          dictionaries_to_output[dict.name] = true
        end
      end

      # If the entity is a Component, show its definition's attributes as well.
      if entity.is_a?(Sketchup::ComponentInstance)
        if entity.definition.attribute_dictionaries != nil
          for dict in entity.definition.attribute_dictionaries
            dictionaries_to_output[dict.name] = true
          end
        end
      end
    end

    top_level_attribute_hash = {}
    json_hash[:attributeDictionaries] = top_level_attribute_hash
    for dict in dictionaries_to_output.keys
      dictionary_hash = {}
      top_level_attribute_hash[dict] = dictionary_hash
      if nest_meta_attributes == true && dict == @dictionary_name
        for key in @dc.get_attribute_list(entity)
          attr_hash = {}
          dictionary_hash[key] = attr_hash
          attr_hash[:value] = @dc.get_attribute_value(entity, key)
          if @dc.get_attribute_formula(entity, key) != nil
            attr_hash[:formula] = @dc.get_attribute_formula(entity, key)
          end
          if @dc.get_attribute_label(entity, key) != nil
            attr_hash[:label] = @dc.get_attribute_label(entity, key)
          end
          if @dc.get_attribute_options(entity, key) != nil
            attr_hash[:options] = @dc.get_attribute_options(entity, key)
          end
          if @dc.get_attribute_access(entity, key) != nil
            attr_hash[:access] = @dc.get_attribute_access(entity, key)
          end
          if @dc.get_attribute_formlabel(entity, key) != nil
            attr_hash[:formlabel] = @dc.get_attribute_formlabel(entity, key)
          end
          if @dc.get_attribute_units(entity, key) != nil
            attr_hash[:units] = @dc.get_attribute_units(entity, key)
          end
          if @dc.get_attribute_error(entity, key) != nil
            attr_hash[:error] = @dc.get_attribute_error(entity, key)
          end
          if @dc.get_attribute_formulaunits(entity, key) != nil
            attr_hash[:formulaunits] = @dc.get_attribute_formulaunits(entity, key)
          end
        end

        length_units = @dc.get_entity_lengthunits(entity, true)
        dictionary_hash[:_iscollapsed] = {
          :value => @dc.get_attribute_value(entity, "_iscollapsed"),
        }
        dictionary_hash[:_lengthunits] = {
          :value => length_units,
        }
        dictionary_hash[:_name] = {
          :value => @dc.get_attribute_value(entity, "_name"),
        }
      elsif Sketchup.is_pro?
        # Classifier Attributes.
        classifier_attrs_hash = {}
        dictionary_hash[dict] = classifier_attrs_hash
        if entity.is_a?(Sketchup::ComponentInstance)
          attrdict = entity.definition.attribute_dictionary dict
        else
          attrdict = entity.attribute_dictionary dict
        end
        add_attribute_dictionary_to_json(classifier_attrs_hash, attrdict)
      end
    end

    # Generate json for subentities.
    if entity.is_a?(Sketchup::ComponentInstance)
      subentities = entity.definition.entities
    else
      subentities = entity.entities
    end

    # This method can continue recursion if max_recursion_depth == -1 or if
    # the next depth is less than or equal to the max_recursion_depth.
    can_go_deeper = (max_recursion_depth == -1)
    if !can_go_deeper
      can_go_deeper = current_depth + 1 <= max_recursion_depth
    end

    subentities_json_list = []
    if subentities != nil && do_recursion.to_s == "true" && can_go_deeper
      for subentity in subentities
        if subentity.is_a?(Sketchup::ComponentInstance) ||
           subentity.is_a?(Sketchup::Group)
          # Never send up json for our "highlight instance"
          if @dc.copy_number_of(subentity) == 0.0 && subentity.name != "(highlight)"
            subentities_json_list.push(
              entity_to_json_hash(subentity, do_recursion, specific_dictionary,
                                  nest_meta_attributes, current_depth + 1, max_recursion_depth)
            )
          end
        end
      end
    end
    json_hash[:subentities] = subentities_json_list

    # If we're requesting a json struct for the entire model, then pull in
    # definitions attributes.
    if entity.is_a?(Sketchup::Model)
      definitions = []
      for definition in entity.definitions
        definitions.push(entity_to_json_hash(
          definition, false, specific_dictionary,
          nest_meta_attributes
        ))
      end
      json_hash[:definitions] = definitions
    end

    # If we're at the top level of a tree, clear out our names hash
    # before returning.
    if clear_names_before_return == true
      @has_used_name = nil
    end
    return json_hash
  end

  def add_attribute_dictionary_to_json(json_hash, attrdict)
    if @dc.display_attribute?(attrdict, @simplify)
      attrdict.each_pair { |key, value|
        json_hash[key] = value
      }
      if attrdict.attribute_dictionaries
        attrdict.attribute_dictionaries.each { |sub_dict|
          next if !@dc.display_attribute?(sub_dict, @simplify)
          json_hash[sub_dict.name] = sub_dict
        }
      end
    end
  end

  # Copied over from dccclass_v1 without all the clean() calls.
  # We don't need to clean the names anymore.
  def get_entity_formula_name(entity)
    # Look for a cached _name on the instance first.
    this_name = @dc.get_instance_attribute(entity, "_name")

    # If no name was found, look for the SU-level name of the item.
    if this_name == "" || this_name == nil
      this_name = entity.name
    end

    # If still no name, then look for a cached _name on the definition.
    if this_name == "" || this_name == nil
      this_name = @dc.get_definition_attribute(entity, "_name")
    end

    # If still no name, then create one from the definition name or the typename
    # of the entity.
    if this_name == "" || this_name == nil
      if entity.is_a?(Sketchup::ComponentInstance)
        this_name = entity.definition.name
      else
        this_name = entity.typename
      end

      # If we're still empty (meaning the name from SU was nothing but invalid
      # characters, then default to the typename.
      if this_name == "" || this_name == nil
        this_name = entity.typename
      end
    end
    return this_name
  end
end
